package edu.bu.cs673b1s1p3.nextbus.map;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.swing.JLabel;

import org.jdesktop.swingx.JXMapKit;
import org.jdesktop.swingx.JXMapKit.DefaultProviders;
import org.jdesktop.swingx.JXMapViewer;
import org.jdesktop.swingx.mapviewer.GeoPosition;
import org.jdesktop.swingx.mapviewer.Waypoint;
import org.jdesktop.swingx.mapviewer.WaypointPainter;
import org.jdesktop.swingx.mapviewer.WaypointRenderer;
import org.jdesktop.swingx.painter.CompoundPainter;
import org.jdesktop.swingx.painter.Painter;

import edu.bu.cs673b1s1p3.nextbus.service.NextBusService;
import edu.bu.cs673b1s1p3.nextbus.service.ServiceUnavailableException;
import edu.bu.cs673b1s1p3.nextbus.service.routeconfig.Path;
import edu.bu.cs673b1s1p3.nextbus.service.routeconfig.RouteConfig;
import edu.bu.cs673b1s1p3.nextbus.service.routeconfig.Stop;
import edu.bu.cs673b1s1p3.nextbus.service.routelist.Route;

/**
 * 
 * @author Jimson
 *
 */
public class MapModel {
	private JXMapKit mapViewer = new JXMapKit();
	private WaypointPainter<JXMapViewer> points = new WaypointPainter<JXMapViewer>();
	private WaypointPainter<JXMapViewer> pointIdentifiers = new WaypointPainter<JXMapViewer>();
	private WaypointPainter<JXMapViewer> homePoints = new WaypointPainter<JXMapViewer>();
	private Painter<JXMapViewer> lines;
	private Set<Waypoint> waypointGroup = new HashSet<Waypoint>();
	private Set<Waypoint> home = new HashSet<Waypoint>();
	//private ArrayList<Painter> painterList = new ArrayList(3);
	private NextBusService nextBus;
	
	ArrayList<GeoPosition> routeStations = new ArrayList<GeoPosition>();
	
	/**
	 * constructor
	 * 
	 */
	public MapModel() {
		
	}
	
	/**
	 * constructor with only map view
	 * 
	 * @param mapViewer
	 */
	public MapModel(JXMapKit mapViewer) {
		this.mapViewer = mapViewer;
		//Added by Lenny
		nextBus = NextBusService.getInstance();
	}
	
	/**
	 * constructor with a map view and a specific agency and route
	 * 
	 * @param mapViewer
	 * @param nextBS
	 * @param route
	 * @throws ServiceUnavailableException 
	 */
	public MapModel(JXMapKit mapViewer, NextBusService nextBS, Route route) throws ServiceUnavailableException {
		this.mapViewer = mapViewer;
		this.routeStations = nextBS.getStopsForDisplay(route);
	}
	
	/**
	 * Constructor with the map viewer and a list of stop positions
	 * 
	 * @param mapViewer
	 * @param routeStations
	 */
	public MapModel(JXMapKit mapViewer, ArrayList<GeoPosition> routeStations) {
		this.mapViewer = mapViewer;
		this.routeStations = routeStations;
	}
	
	/**
	 * Set a list of points into the array list to used to draw a full route.
	 * Points are extracted from paths in a specific route.
	 * 
	 * @param nextBS next bus service
	 * @param routeConfig the configuration of a specific agency, route
	 * @param pathIndex the index of a path list
	 */
	public void setPaths(NextBusService nextBS, RouteConfig routeConfig, int pathIndex) {
		this.routeStations = nextBS.getPointsForDisplay(routeConfig, pathIndex);
	}
	
	/**
	 * 
	 * @param nextBS
	 * @param route
	 * @throws ServiceUnavailableException 
	 */
	public void setRouteStations(NextBusService nextBS, Route route) throws ServiceUnavailableException
	{
		this.routeStations = nextBS.getStopsForDisplay(route);
	}
	
	/**
	 * Sets a list of points into the array list to be used to draw a route.
	 * Points are all the stops in a specific route.
	 * 
	 * @param routeStations a list of stops with longitudes and latitudes.
	 */
	public void setRouteStations(ArrayList<GeoPosition> routeStations)
	{
		this.routeStations = routeStations;
	}
	
	/**
	 * Gets the current map viewer.
	 * 
	 * @return mapViewer current map viewer.
	 */
	public JXMapKit getMapViewer() {
		return mapViewer;
	}
	
	/**
	 * Gets the default map provider OpenStreetMaps, could be
	 * changed when override it.
	 * 
	 * @return DefaultMapProvider OpenStreetMaps.
	 */
	public DefaultProviders getMapProvider() {
		return DefaultProviders.OpenStreetMaps;
	}
	
	/**
	 * Sets stop position
	 * 
	 * @param stopPos
	 */
	public void setStop(GeoPosition stopPos) {
		mapViewer.setAddressLocation(stopPos);
	}
	
	/**
	 * Clears all overlays and provides an empty map
	 */
	public void clearOverlays() {
		// clear previous bus stops first
		waypointGroup.clear();
		points.setWaypoints(waypointGroup);
		pointIdentifiers.setWaypoints(waypointGroup);
		home.clear();
		homePoints.setWaypoints(home);
		//lines = null;
		mapViewer.getMainMap().setOverlayPainter(null);
	}
	
	
	
	/**
	 * Displays home location and closest stops
	 */
	public void showStopsNearHome() {
		CompoundPainter<JXMapViewer> cp = new CompoundPainter<JXMapViewer>();
		
		//Draw specified points (closest bus stops) and home (entered address) point
		cp.setPainters(points, pointIdentifiers, homePoints);
		cp.setCacheable(false);
		mapViewer.getMainMap().setOverlayPainter(cp);
	}
	
	
	/**
	 * Displays multiple overlays
	 */
	public void showAllOverlays() {
		CompoundPainter<JXMapViewer> cp = new CompoundPainter<JXMapViewer>();
/*		Painter[] painters = new Painter[painterList.size()];
		for (int i=0; i<painterList.size(); i++) {
			painters[i] = painterList.get(i);
		}*/
		
		//Draw routes, specified stops, home (entered address) point
		cp.setPainters(lines, points, pointIdentifiers, homePoints);
		cp.setCacheable(false);
		mapViewer.getMainMap().setOverlayPainter(cp);
	}
	
	
	/**
	 * Adds a single waypoint to indicate a marker on the map.
	 * 
	 * @param waypoint a waypoint to be drawn on map
	 */
	public void addWaypoint(Waypoint waypoint) {
		waypointGroup.add(waypoint);
		points.setWaypoints(waypointGroup);
		//painterList.add(points);
	}
	
	/**
	 * Adds a set of waypoints to indicate a marker on the map.
	 * 
	 * @param waypoints a list of waypoints to be drawn on map
	 */
	public void addWaypoint(final Set<Waypoint> waypoints, final List<Stop> stops) {
		final Font f = new Font("SansSerif", Font.BOLD, 8);
		
		points.setWaypoints(waypoints);
		pointIdentifiers.setWaypoints(waypoints);
		pointIdentifiers.setRenderer(new WaypointRenderer() {
			public boolean paintWaypoint(Graphics2D g, JXMapViewer map, Waypoint wp) {
				int stopIndex = getWaypointIndex(stops, wp);
				String id = Integer.toString(stopIndex + 1);
				g.setFont(f);
				g.drawString(id, -4, -20);
				return true;
			}
		});
		
		//painterList.add(points);
	}
	
	/**
	 * Identifies the index of a passed in waypoint
	 * 
	 * @param stops a list of stops includes the waypoint
	 * @param wp the waypoint to be identified
	 * @return index the index of the waypoint
	 */
	public int getWaypointIndex(List<Stop> stops, Waypoint wp) {
		int i = 0;
		for (Stop s : stops) {
			double latDif = wp.getPosition().getLatitude() - s.getLat().doubleValue();
			double lonDif = wp.getPosition().getLongitude() - s.getLon().doubleValue();
			if (Math.abs(latDif) <= 0.0000000001 &&
					Math.abs(lonDif) <= 0.0000000001) {
				return i;
			}
			i++;
		}
		return -1;
	}
	
	/**
	 * Adds labels for stops depends on mouse motion
	 * 
	 * @param stop
	 */
	public void addWaypointLabel(final Stop stop) {
		final JLabel stopLabel = new JLabel(stop.getTitle());
		stopLabel.setForeground(Color.RED);
		stopLabel.setVisible(false);
		mapViewer.getMainMap().add(stopLabel);
		
		mapViewer.getMainMap().addMouseMotionListener(new MouseMotionListener() {
			public void mouseDragged(MouseEvent e) {}
			
			public void mouseMoved(MouseEvent e) {
				JXMapViewer map = mapViewer.getMainMap();
				
				// location of stop
				GeoPosition gp = new GeoPosition(stop.getLat().doubleValue(), 
						stop.getLon().doubleValue());
				
				// convert to world bitmap
				Point2D gp_pt = map.getTileFactory().geoToPixel(gp, map.getZoom());
				
				// convert to screen
				Rectangle rect = map.getViewportBounds();
				Point converted_gp_pt = new Point((int)gp_pt.getX()-rect.x,
						(int)gp_pt.getY()-rect.y);
				
				//check if near the mouse
				if(converted_gp_pt.distance(e.getPoint()) < 25) {
					stopLabel.setLocation(converted_gp_pt);
					stopLabel.setVisible(true);
				} else {
					stopLabel.setVisible(false);
				}
			}
		});
	}
	
	/**
	 * Adds a single waypoint to indicate Home location.
	 * 
	 * @param waypoint a waypoint to be drawn on map
	 */
	public void addHomeWaypoint(Waypoint waypoint) {
		
		home.add(waypoint);
		homePoints.setWaypoints(home);
		homePoints.setRenderer(new WaypointRenderer() {
		    public boolean paintWaypoint(Graphics2D g, JXMapViewer map, Waypoint wp) {
		    	g.setStroke(new BasicStroke(3f));
	            g.setColor(Color.BLACK);
	            g.drawOval(-10,-10,20,20);
	            g.setStroke(new BasicStroke(1f));
	            g.drawLine(-10,0,10,0);
	            g.drawLine(0,-10,0,10);
		        return true;
		    }
		});	
		//painterList.add(homePoints);
	}
	

	/**
	 * Draw the bus route using a list of longitudes and latitudes
	 * 
	 */
	public void drawRoute() {
		// modify paint method to create route on wikimap
		final Painter<JXMapViewer> lineOverlay = new Painter<JXMapViewer> () {
			
			@Override
			public void paint(Graphics2D g, final JXMapViewer map, final int w, final int h) {
				g = (Graphics2D) g.create();
				
				// convert from viewport to world bitmap
				final Rectangle rect = mapViewer.getMainMap().getViewportBounds();
				g.translate(-rect.x, -rect.y);
				
				// do the drawing
				g.setColor(Color.RED);
				g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
				g.setStroke(new BasicStroke(4));
				
				int lastX = -1;
				int lastY = -1;
				for (final GeoPosition gp : routeStations) {
					// convert geo to world bitmap pixel
					final Point2D pt = mapViewer.getMainMap().getTileFactory().geoToPixel(gp, mapViewer.getMainMap().getZoom());
					if (lastX != -1 && lastY != -1) {
						g.drawLine(lastX, lastY, (int) pt.getX(), (int) pt.getY());
					}
					lastX = (int) pt.getX();
					lastY = (int) pt.getY();
				}				
				g.dispose();
			}
		};
		mapViewer.setCenterPosition(routeStations.get(0));
		lines = lineOverlay;
	}
	
	/**
	 * Draw a full route using the list of paths from a specific bus route.
	 * The paths are converted to GeoPosition objects for display.
	 * 
	 * @param routeConfig the configuration based on an agency, a bus route
	 * @param paths the list of paths {@link path}} from the bus route
	 */
	public void drawRoute(final RouteConfig routeConfig, final ArrayList<Path> paths) {
		final NextBusService nextBus = this.nextBus; 
		
		// set the center point to be the position of the first index of path
		GeoPosition center = nextBus.getPointsForDisplay(routeConfig, 0).get(0);
		
		// clear previous bus stops
		waypointGroup.clear();
		points.setWaypoints(waypointGroup);
		pointIdentifiers.setWaypoints(waypointGroup);
		
		// clear home stops
		home.clear();
		homePoints.setWaypoints(home);
		
		// modify paint method to create route on wikimap
		Painter<JXMapViewer> lineOverlay = new Painter<JXMapViewer> () {
			
			@Override
			public void paint(Graphics2D g, final JXMapViewer map, final int w, final int h) {
				g = (Graphics2D) g.create();
				
				// convert from viewport to world bitmap
				final Rectangle rect = mapViewer.getMainMap().getViewportBounds();
				g.translate(-rect.x, -rect.y);
				
				// do the drawing
				g.setColor(Color.RED);
				g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
				g.setStroke(new BasicStroke(4));
				
				
				ArrayList<GeoPosition> geoPath = new ArrayList<GeoPosition>();
				
				//Loop through all the paths in routeConfig and get the points
				for (int i=0; i<paths.size(); i++) {
					
					geoPath = nextBus.getPointsForDisplay(routeConfig, i);
					
					int lastX = -1;
					int lastY = -1;
					for (final GeoPosition gp : geoPath) {
						// convert geo to world bitmap pixel
						final Point2D pt = mapViewer.getMainMap().getTileFactory().geoToPixel(gp, mapViewer.getMainMap().getZoom());
						if (lastX != -1 && lastY != -1) {
							g.drawLine(lastX, lastY, (int) pt.getX(), (int) pt.getY());
						}
						lastX = (int) pt.getX();
						lastY = (int) pt.getY();
					}
				}
				g.dispose();
			}
		};
		mapViewer.setCenterPosition(center);
		lines = lineOverlay;
		//painterList.add(lines);
	}
}
